package com.studerb.service;

import static com.studerb.util.Constants.EVENT_SERVICE_NAME;

import java.net.SocketException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.net.nntp.NNTPConnectionClosedException;
import org.apache.log4j.Logger;
import org.bushe.swing.event.annotation.AnnotationProcessor;
import org.bushe.swing.event.annotation.EventSubscriber;
import org.springframework.beans.factory.annotation.Autowired;

import com.studerb.events.NetworkMonitorEvent;
import com.studerb.events.NntpConnectionEvent;
import com.studerb.nntp.NetworkMonitorThread;
import com.studerb.service.interfaces.INetworkMonitorService;
import com.studerb.service.interfaces.IServerService;

/**
 * 
 * The network monitor runs in a infinite loop checking the network status.
 * <p>
 * If the network is working, the monitor will sleep in-between invocations of
 * the server call; this sleep time is settable through the app settings.
 * <p>
 * If the network is down and/or the network has received some number (also set
 * via app properties) of {@link com.studerb.NntpConnectionEvent
 * NntpConnectionEvent} where event type is <code>Error</code>, the monitor will
 * sleep a shorter time (modifiable via app setting) and check the network.
 * 
 * If a successful server connection is made, the public static method
 * {@link #networkUp isNetworkUp()} will return true. This variable is always
 * set to false initially until the first successful network connection is made.
 * The {@link #networkUp isNetworkUp()} method should only be called after
 * obtaining the static lock field {@link networkLock}. If the network is not up
 * any code depending on the network should wait (block) on the static condition
 * {@link networkUP} which is derived from the lock {@link networkLock}. When
 * the network transitions from down -> up (after a successful ntp connection),
 * all threads blocking on the codition will be notified. The lock
 * {@link networkLock} guarding {@link networkUp} is important in order to avoid
 * a potential race condition where a thread sees {@link networkUp} at false and
 * then blocks on the {@link networkUP} condition - in-between checking the
 * boolean value and blocking, the thread my be pauses and the monitor could
 * then set the network to UP. The blocking thread would then be never woken up
 * in worst case, or in best case, would have to wait until the next down -> up
 * transition to be notified.
 * 
 */

public class NetworkMonitorService implements INetworkMonitorService {
	private final Logger logger = Logger.getLogger(NetworkMonitorService.class);
	private boolean started = false;
	private Thread t;

	private final ReentrantLock networkLock = new ReentrantLock();
	private final Condition networkUP = networkLock.newCondition();
	private final Condition networkDOWN = networkLock.newCondition();

	private final AtomicInteger currentErrors = new AtomicInteger(0);
	protected boolean networkUp = false;
	@Autowired EventService eventService;
	@Autowired IServerService serverService;

	@Autowired NetworkMonitorThread networkMonitorThread;

	public void awaitNetworkDown() throws InterruptedException {
		networkLock.lock();
		try {
			while (networkUp) {
				networkDOWN.await();
			}
		}
		finally {
			networkLock.unlock();
		}
	}

	public void awaitNetworkUp() throws InterruptedException {
		networkLock.lock();
		try {
			while (!networkUp) {
				networkUP.await();
			}
		}
		finally {
			networkLock.unlock();
		}
	}

	public int getCurrentErrors() {
		return currentErrors.intValue();
	}

	public boolean isNetworkUp() {
		return networkUp;
	}

	@Override public boolean isStarted() {
		return started;
	}

	@EventSubscriber(eventServiceName = EVENT_SERVICE_NAME) public void onEvent(NntpConnectionEvent event) {
		if (!isStarted()) {
			return;
		}
		if (event.getType() == NntpConnectionEvent.EventType.EXCEPTION) {
			Throwable t = event.getThrowable();
			if (t instanceof NNTPConnectionClosedException || t instanceof SocketException) {
				logger.debug("Got NntpConnection Exception: " + ExceptionUtils.getRootCauseMessage(t));
				int current = currentErrors.incrementAndGet();
				logger.debug("Current Error Count: " + current);
				setNetworkDown();
			}
		}
	}

	private void setNetworkDown() {
		if (!networkUp) {
			return;
		}
		logger.info("Turning Network: DOWN");
		networkLock.lock();
		try {
			networkUp = false;
			networkDOWN.signal();
			eventService.publish(new NetworkMonitorEvent(NetworkMonitorEvent.EventType.NETWORK_DOWN));
		}
		finally {
			networkLock.unlock();
		}
	}

	public void setNetworkUp() {
		if (networkUp) {
			return;
		}
		logger.info("Turning Network: UP");
		networkLock.lock();
		try {
			networkUp = true;
			logger.debug("Signalling to all: NetworkUP");
			currentErrors.set(0);
			networkUP.signalAll();
			eventService.publish(new NetworkMonitorEvent(NetworkMonitorEvent.EventType.NETWORK_UP));
		}
		finally {
			networkLock.unlock();
		}
	}

	@Override public void start() {
		if (started)
			throw new RuntimeException("NewsGroupService already Started");
		logger.info("[**NetworkMonitorService Starting**]");
		try {
			t = new Thread(networkMonitorThread);
			logger.debug("Starting NetworkMonitorThread");
			t.start();
			started = true;
			AnnotationProcessor.process(this);
		}
		catch (Exception e) {
			started = false;
			throw new RuntimeException("Error Starting NetworkMonitorService", e);
		}
	}

	@Override public void stop() {
		if (!started)
			throw new RuntimeException("NewsGroupService already Started");
		logger.info("[**NewsGroupService Stopping**]");
		logger.debug("Interrupting NetworkMonitorThread");
		t.interrupt();
		started = false;
		// TODO Auto-generated method stub
	}
}
